// Copyright 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef COMPONENTS_SESSIONS_CORE_COMMAND_STORAGE_BACKEND_H_
#define COMPONENTS_SESSIONS_CORE_COMMAND_STORAGE_BACKEND_H_

#include <stddef.h>

#include <memory>
#include <set>
#include <vector>

#include "base/callback_forward.h"
#include "base/files/file_path.h"
#include "base/memory/ref_counted_delete_on_sequence.h"
#include "components/sessions/core/command_storage_manager.h"
#include "components/sessions/core/session_command.h"
#include "components/sessions/core/sessions_export.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

namespace base {
class File;
}

namespace crypto {
class Aead;
}

namespace sessions {

// CommandStorageBackend is the backend used by CommandStorageManager. It writes
// SessionCommands to disk with the ability to read back at a later date.
// CommandStorageBackend (mostly) does not interpret the commands in any way, it
// simply reads/writes them.
//
// CommandStorageBackend writes to a file with a suffix that indicates the
// time the file was opened. The time stamp allows this code to determine the
// most recently written file. When AppendCommands() is supplied a value of true
// for `truncate`, the current file is closed and a new file is created (with
// a newly generated timestamp). When AppendCommands() successfully writes the
// commands to the file an internal command (whose id is
// `kInitialStateMarkerCommandId`) is written. During startup, the most recent
// file that has the internal command written is used. This ensures restore does
// not attempt to use a file that did not have the complete state written
// (this would happen if chrome crashed while writing the commands, or there
// was a file system error part way through writing).
//
// AppendCommands() takes a callback that is called if there is an error in
// writing to the file. The expectation is if there is an error, the consuming
// code must call AppendCommands() again with `truncate` set to true. If there
// was an error in writing to the file, calls to AppendCommands() with a value
// of false for `truncate` are ignored. This is done to ensure the consuming
// code correctly supplies the initial state.
class SESSIONS_EXPORT CommandStorageBackend
    : public base::RefCountedDeleteOnSequence<CommandStorageBackend> {
 public:
  struct SESSIONS_EXPORT ReadCommandsResult {
    ReadCommandsResult();
    ReadCommandsResult(ReadCommandsResult&& other);
    ReadCommandsResult& operator=(ReadCommandsResult&& other);
    ReadCommandsResult(const ReadCommandsResult&) = delete;
    ReadCommandsResult& operator=(const ReadCommandsResult&) = delete;
    ~ReadCommandsResult();

    std::vector<std::unique_ptr<sessions::SessionCommand>> commands;
    bool error_reading = false;
  };

  using id_type = SessionCommand::id_type;
  using size_type = SessionCommand::size_type;

  // Initial size of the buffer used in reading the file. This is exposed
  // for testing.
  static const int kFileReadBufferSize;

  // Number of bytes encryption adds.
  static const size_type kEncryptionOverheadInBytes;

  // Represents data for a session. Public for tests.
  // Creates a CommandStorageBackend. This method is invoked on the MAIN thread,
  // and does no IO. The real work is done from Init, which is invoked on
  // a background task runer.
  //
  // See `CommandStorageManager` for details on `type` and `path`.
  CommandStorageBackend(
      scoped_refptr<base::SequencedTaskRunner> owning_task_runner,
      const base::FilePath& path,
      CommandStorageManager::SessionType type,
      const std::vector<uint8_t>& decryption_key = {});
  CommandStorageBackend(const CommandStorageBackend&) = delete;
  CommandStorageBackend& operator=(const CommandStorageBackend&) = delete;

  // Returns true if the file at |path| was generated by this class.
  static bool IsValidFile(const base::FilePath& path);

  // Returns the path the files are being written to.
  const base::FilePath& current_path() const { return current_path_; }

  base::SequencedTaskRunner* owning_task_runner() {
    return base::RefCountedDeleteOnSequence<
        CommandStorageBackend>::owning_task_runner();
  }

  // Appends the specified commands to the current file. If |truncate| is true
  // the file is truncated. If |truncate| is true and |crypto_key| is non-empty,
  // then all commands are encrypted using the supplied key. If there is an
  // error writing the commands, `error_callback` is run.
  void AppendCommands(
      std::vector<std::unique_ptr<sessions::SessionCommand>> commands,
      bool truncate,
      base::OnceClosure error_callback,
      const std::vector<uint8_t>& crypto_key = std::vector<uint8_t>());

  bool inited() const { return inited_; }

  // Parses out the timestamp from a path pointing to a session file.
  static bool TimestampFromPath(const base::FilePath& path, base::Time& result);

  // Returns the set of possible session files. The returned paths are not
  // necessarily valid session files, rather they match the naming criteria
  // for session files.
  static std::set<base::FilePath> GetSessionFilePaths(
      const base::FilePath& path,
      CommandStorageManager::SessionType type);

  // Returns the commands from the last session file.
  ReadCommandsResult ReadLastSessionCommands();

  // Deletes the file containing the commands for the last session.
  void DeleteLastSession();

  // Moves the current session file to the last session file. This is typically
  // called during startup or if the user launches the app and no tabbed
  // browsers are running. After calling this, set_pending_reset() must be
  // called.
  void MoveCurrentSessionToLastSession();

  // Used in testing to emulate an error in writing to the file. The value is
  // automatically reset after the failure.
  void ForceAppendCommandsToFailForTesting();

 private:
  friend class base::RefCountedDeleteOnSequence<CommandStorageBackend>;
  friend class base::DeleteHelper<CommandStorageBackend>;
  friend class CommandStorageBackendTest;

  struct SessionInfo {
    base::FilePath path;
    base::Time timestamp;
  };

  ~CommandStorageBackend();

  // Performs initialization on the background task run, calling DoInit() if
  // necessary.
  void InitIfNecessary();

  // Generates the path to a session file with the given timestamp.
  static base::FilePath FilePathFromTime(
      CommandStorageManager::SessionType type,
      const base::FilePath& path,
      base::Time time);

  // Reads the commands from the specified file.  If |crypto_key| is non-empty,
  // it is used to decrypt the file.
  static ReadCommandsResult ReadCommandsFromFile(
      const base::FilePath& path,
      const std::vector<uint8_t>& crypto_key);

  // Closes the file. The next time AppendCommands() is called the file will
  // implicitly be reopened.
  void CloseFile();

  // If current_session_file_ is open, it is truncated so that it is essentially
  // empty (only contains the header). If current_session_file_ isn't open, it
  // is is opened and the header is written to it. After this
  // current_session_file_ contains no commands.
  // NOTE: current_session_file_ may be null if the file couldn't be opened or
  // the header couldn't be written.
  void TruncateOrOpenFile();

  // Opens the current file and writes the header. On success a handle to
  // the file is returned.
  std::unique_ptr<base::File> OpenAndWriteHeader(
      const base::FilePath& path) const;

  // Appends the specified commands to the specified file.
  bool AppendCommandsToFile(
      base::File* file,
      const std::vector<std::unique_ptr<sessions::SessionCommand>>& commands);

  // Writes |command| to |file|. Returns true on success.
  bool AppendCommandToFile(base::File* file,
                           const sessions::SessionCommand& command);

  // Encrypts |command| and writes it to |file|. Returns true on success.
  // The contents of the command and id are encrypted together. This is
  // preceded by the length of the command.
  bool AppendEncryptedCommandToFile(base::File* file,
                                    const sessions::SessionCommand& command);

  // Returns true if commands are encrypted.
  bool IsEncrypted() const { return !crypto_key_.empty(); }

  // Gets data for the last session file.
  absl::optional<SessionInfo> FindLastSessionFile() const;

  // Attempt to delete all sessions besides the current and last. This is a
  // best effort operation.
  void DeleteLastSessionFiles() const;

  // Gets all sessions files.
  std::vector<SessionInfo> GetSessionFilesSortedByReverseTimestamp() const {
    return GetSessionFilesSortedByReverseTimestamp(supplied_path_, type_);
  }
  static std::vector<SessionInfo> GetSessionFilesSortedByReverseTimestamp(
      const base::FilePath& path,
      CommandStorageManager::SessionType type);

  static bool CompareSessionInfoTimestamps(const SessionInfo& a,
                                           const SessionInfo& b) {
    return b.timestamp < a.timestamp;
  }

  // Returns true if `path` can be used for the last session.
  bool CanUseFileForLastSession(const base::FilePath& path) const;

  const CommandStorageManager::SessionType type_;

  // This is the path supplied to the constructor. See CommandStorageManager
  // constructor for details.
  const base::FilePath supplied_path_;

  // Used to decode the initial last session file.
  // TODO(sky): this is currently required because InitIfNecessary() determines
  // the last file. If that can be delayed, then this can be supplied to
  // GetLastSessionCommands().
  const std::vector<uint8_t> initial_decryption_key_;

  // TaskRunner that the callback is added to.
  scoped_refptr<base::SequencedTaskRunner> callback_task_runner_;

  // Path commands are currently being saved to.
  base::FilePath current_path_;

  // This may be null, created as necessary.
  std::unique_ptr<base::File> file_;

  // Whether DoInit() was called. DoInit() is called on the background task
  // runner.
  bool inited_ = false;

  std::vector<uint8_t> crypto_key_;
  std::unique_ptr<crypto::Aead> aead_;

  // Incremented every time a command is written.
  int commands_written_ = 0;

  // Set to true once `kInitialStateMarkerCommandId` is written.
  bool did_write_marker_ = false;

  // Timestamp when this session was started.
  base::Time timestamp_;

  // Data for the last session. If unset, fallback to legacy session data.
  absl::optional<SessionInfo> last_session_info_;

  absl::optional<base::FilePath> last_file_with_valid_marker_;

  bool force_append_commands_to_fail_for_testing_ = false;
};

}  // namespace sessions

#endif  // COMPONENTS_SESSIONS_CORE_COMMAND_STORAGE_BACKEND_H_
